package edu.kit.csl.cratylus.extraction;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import de.lmu.ifi.dbs.elki.algorithm.AbstractAlgorithm;
import de.lmu.ifi.dbs.elki.algorithm.clustering.DBSCAN;
import de.lmu.ifi.dbs.elki.algorithm.clustering.DeLiClu;
import de.lmu.ifi.dbs.elki.algorithm.clustering.OPTICS;
import de.lmu.ifi.dbs.elki.algorithm.clustering.OPTICSXi;
import de.lmu.ifi.dbs.elki.algorithm.clustering.SLINK;
import de.lmu.ifi.dbs.elki.data.Cluster;
import de.lmu.ifi.dbs.elki.data.Clustering;
import de.lmu.ifi.dbs.elki.data.model.Model;
import de.lmu.ifi.dbs.elki.database.Database;
import de.lmu.ifi.dbs.elki.database.StaticArrayDatabase;
import de.lmu.ifi.dbs.elki.database.ids.DBID;
import de.lmu.ifi.dbs.elki.datasource.filter.ObjectFilter;
import de.lmu.ifi.dbs.elki.distance.distancevalue.IntegerDistance;
import de.lmu.ifi.dbs.elki.index.IndexFactory;
import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.deliclu.DeLiCluTreeFactory;
import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.ListParameterization;
import edu.kit.csl.cratylus.datatypes.BadParameterValueException;
import edu.kit.csl.cratylus.datatypes.Candidate;
import edu.kit.csl.cratylus.extraction.elki.CandidateDatabaseConnection;
import edu.kit.csl.cratylus.extraction.elki.ElkiCandidate;
import edu.kit.csl.cratylus.extraction.elki.LevenshteinDistanceFunction;
import edu.kit.csl.cratylus.extraction.elki.RelativeLevenshteinDistanceFunction;
import edu.kit.csl.cratylus.io.Logger;
import edu.kit.csl.cratylus.ui.Configuration;

/**
 * This splitter uses the clustering library ELKI to cluster the candidates.
 * The ELKI project and further documentation can be downloaded from the 
 * <a href="http://elki.dbs.ifi.lmu.de/">ELKI project homepage</a>. The 
 * integration of ELKI in cratylus requires some helper classes, which are
 * coupled in the package {@code edu.kit.csl.cratylus.extraction.elki}. 
 */
public class ElkiSplitter implements Splitter {

	AbstractAlgorithm<Clustering<Model>> algo;
	
	private static final int MAX_ELEMENTS = 5000;
	private Splitter fallbackSplitter;
	private Logger log;
	private ListParameterization indexparams;
	
	/**
	 * Sole constructor. Fetches the splitter and ELKI related parameters and
	 * initializes the clustering algorithm.
	 * 
	 * @throws BadParameterValueException if the clustering algorithm is 
	 * 		unknown, or its configuration is invalid
	 */
	public ElkiSplitter() throws BadParameterValueException {
		Configuration conf = Configuration.getSingleton();
		log = Logger.getSingleton();
		String algoName = conf.getString("splitterAlgorithm");
		ListParameterization params = new ListParameterization();
		indexparams = new ListParameterization();
		if (algoName.equals("DBSCAN")) {
			configureDbscan(params);
		} else if (algoName.equals("DeLiClu")) {
			configureDeLiClu(params);
		} else if (algoName.equals("OPTICS")) {
			configureOptics(params);
		} else if (algoName.equals("SLINK")) {
			configureSlink(params);
		} else {
			throw new BadParameterValueException("Unknown splitter algorithm "
					+ algoName);
		}
	    fallbackSplitter = new MarxSplitter();
	}
	
	/*
	 * Configure DBSCAN algorithm.
	 */
	private void configureDbscan(ListParameterization params) 
			throws BadParameterValueException {
		Configuration conf = Configuration.getSingleton();
		params.addParameter(DBSCAN.DISTANCE_FUNCTION_ID, getDistanceClass());
		params.addParameter(DBSCAN.EPSILON_ID, 
				new IntegerDistance(conf.getInteger("splitterParamEpsilon")));
		params.addParameter(DBSCAN.MINPTS_ID,
				conf.getInteger("splitterParamMinPts"));
		// DBSCAN<ElkiCandidate, IntegerDistance> dbscan
		algo = ClassGenericsUtil.parameterizeOrAbort(DBSCAN.class, params);
	}
	
	/*
	 * Configure DeLiClu algorithm.
	 */
	private void configureDeLiClu(ListParameterization params) 
			throws BadParameterValueException {
		Configuration conf = Configuration.getSingleton();
		params.addParameter(DeLiClu.DISTANCE_FUNCTION_ID, getDistanceClass());
		params.addParameter(DeLiClu.MINPTS_ID,
				conf.getInteger("splitterParamMinPts"));
		params.addParameter(OPTICSXi.XI_ID, 0.038);
		params.addParameter(OPTICSXi.XIALG_ID, DeLiClu.class);
		indexparams.addParameter(StaticArrayDatabase.INDEX_ID,
				DeLiCluTreeFactory.class);
		algo = ClassGenericsUtil.parameterizeOrAbort(OPTICSXi.class, params);
	}
	
	/*
	 * Configure OPTICS algorithm.
	 */
	private void configureOptics(ListParameterization params) 
			throws BadParameterValueException {
		Configuration conf = Configuration.getSingleton();
		params.addParameter(OPTICS.DISTANCE_FUNCTION_ID, getDistanceClass());
		params.addParameter(OPTICS.EPSILON_ID, 
				new IntegerDistance(conf.getInteger("splitterParamEpsilon")));
		params.addParameter(OPTICS.MINPTS_ID,
				conf.getInteger("splitterParamMinPts"));
		params.addParameter(OPTICSXi.XI_ID, 0.038);
		params.addParameter(OPTICSXi.XIALG_ID, OPTICS.class);
		algo = ClassGenericsUtil.parameterizeOrAbort(OPTICSXi.class, params);
	}
	
	/*
	 * Configure SLINK algorithm.
	 */
	private void configureSlink(ListParameterization params) 
			throws BadParameterValueException {
		params.addParameter(SLINK.DISTANCE_FUNCTION_ID, getDistanceClass());
		params.addParameter(SLINK.SLINK_MINCLUSTERS_ID, 1);
		algo = ClassGenericsUtil.parameterizeOrAbort(SLINK.class, params);
	}
	
	/*
	 * Helper function for configure* functions: Returns the class id of the
	 * distance function
	 */
	private Object getDistanceClass() throws BadParameterValueException {
		String distanceName = Configuration
				.getSingleton().getString("splitterDistanceFunction");
		if (distanceName.equals("levenshtein")) {
			return LevenshteinDistanceFunction.class;
		} else if (distanceName.equals("relativeLevenshtein")) {
			return RelativeLevenshteinDistanceFunction.class;
		} else {
			throw new BadParameterValueException("Unknown distance metric "
					+ distanceName);
		}
	}
	
	/**
	 * Split the candidate set of the planner into clusters found by ELKI.
	 * The clustering algorithm and parameters for ELKI are accessible
	 * via the cratylus command line and processed in the constructor.
	 */
	@Override
	public List<List<Candidate>> split(List<Candidate> candidates) {
		List<List<Candidate>> ret = new LinkedList<List<Candidate>>();
		if (candidates.size() < 3) {
			ret.add(candidates);
			return ret;
		}
		if (candidates.size() > MAX_ELEMENTS) {
			log.warn(
					"Too many candidates for ELKI splitter:" + " "
							+ candidates.size() + ". Falling back to Marx "
							+ "splitter.");
			return fallbackSplitter.split(candidates);
		};
		Database db = new StaticArrayDatabase(new CandidateDatabaseConnection(
				candidates, new ArrayList<ObjectFilter>()),
				new HashSet<IndexFactory<?, ?>>());
		db.initialize();
		Clustering<?> result = algo.run(db);
		for (Cluster<?> cl : result.getAllClusters()) {
			final boolean noise = cl.isNoise();
			LinkedList<Candidate> cluster = new LinkedList<Candidate>();
			for (DBID dbid : cl.getIDs()) {
				Candidate candidate = ((ElkiCandidate) db.getBundle(dbid)
						.data(1)).getCandidate();
				candidate.setNoise(noise);
				cluster.add(candidate);
			}
			if (cluster.size() > 0) {
				ret.add(cluster);
			}
		}
		if (ret.size() > 1 && log.getLevel() <= Logger.DEBUG) {
			log.debug("Clustered " + candidates.size() + " candidates into "
				+ ret.size() + " clusters");
			for (List<Candidate> cluster : ret) {
				StringBuilder b = new StringBuilder();
				for (Candidate c : cluster) {
					b.append("\t" + c.getWord().getContent());
					if (c.isNoise()) {
						b.append("(NOISE)");
					}
				}
				log.debug("CLUSTER:" + b.toString());
			}
		}
		return ret;
	}
}
